﻿using System;
using System.IO;
using System.Xml;
using System.Windows;
using System.Threading;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Reflection;
using System.Runtime.Serialization;

using SBPweb.Forge.Contracts.Services;

namespace SBPweb.Forge.Communication.WCF
{

    public class PollingDuplexServiceClient
    {

        #region Private fields

        private SynchronizationContext threadUI = null;
        private IChannelFactory<IDuplexSessionChannel> channelFactory;
        private Dictionary<Type, Dictionary<object, MulticastDelegate>> dictMessageHandlers = new Dictionary<Type, Dictionary<object, MulticastDelegate>>();

        #endregion

        #region Constructor logic

        public PollingDuplexServiceClient(string relativeServiceUri)
            : this(relativeServiceUri, "forge.SBPweb.hu/IPollingDuplexServerService/Register", null)
        {
        }

        public PollingDuplexServiceClient(string relativeServiceUri, string sessionId)
            : this(GetServiceAbsolutePath(relativeServiceUri), "forge.SBPweb.hu/IPollingDuplexServerService/Register", null, sessionId)
        {
        }

        public PollingDuplexServiceClient(string relativeServiceUri, string action, string actionData)
            : this(GetServiceAbsolutePath(relativeServiceUri), action, actionData, null)
        {
        }

        public PollingDuplexServiceClient(string url, string action, string actionData, string sessionId)
        {
            ServiceUrl = url;
            Action = action;
            ActionData = actionData;
            SessionId = sessionId;
            threadUI = SynchronizationContext.Current;
        }

        #endregion

        #region Property accessors

        public string ServiceUrl
        {
            get;
            set;
        }

        public string Action
        {
            get;
            set;
        }

        public string ActionData
        {
            get;
            set;
        }

        public string SessionId
        {
            get;
            set;
        }

        #endregion

        #region Events

        #endregion

        #region Methods

        public virtual void Start()
        {
            // Instantiate the binding and set the time-outs
            PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding()
            {
                ReceiveTimeout = TimeSpan.FromMinutes(1),
                InactivityTimeout = TimeSpan.FromMinutes(2)
            };

            // Instantiate and open channel factory from binding
            channelFactory = binding.BuildChannelFactory<IDuplexSessionChannel>(new BindingParameterCollection());

            IAsyncResult factoryOpenResult = channelFactory.BeginOpen(new AsyncCallback(OnOpenCompleteFactory), channelFactory);
            if (factoryOpenResult.CompletedSynchronously)
            {
                CompleteOpenFactory(factoryOpenResult);
            }
        }

        public virtual void Stop()
        {
            IAsyncResult factoryCloseResult = this.channelFactory.BeginClose(new AsyncCallback(OnCloseCompleteFactory), channelFactory);
            if (factoryCloseResult.CompletedSynchronously)
            {
                CompleteCloseFactory(factoryCloseResult);
            }
            dictMessageHandlers.Clear();
        }

        #region Handler handling :)
        
        public void AddHandler<TMessageContent>(EventHandler<MessageReceiveEventArgs<TMessageContent>> handler)
        {
            AddHandler<TMessageContent, object>(handler, typeof(TMessageContent));
        }

        public void AddHandler<TMessageContent, TCommand>(EventHandler<MessageReceiveEventArgs<TMessageContent>> handler, TCommand command)
        {
            Type type = typeof(TMessageContent);
            Dictionary<object, MulticastDelegate> dictDelegates;
            if (!dictMessageHandlers.ContainsKey(type))
            {
                dictMessageHandlers.Add(type, dictDelegates = new Dictionary<object, MulticastDelegate>());
            }
            else
            {
                dictDelegates = dictMessageHandlers[type];
            }
            if (dictDelegates.ContainsKey(command))
            {
                MulticastDelegate md = dictDelegates[type];
                dictDelegates[command] = (MulticastDelegate)MulticastDelegate.Combine(md, (MulticastDelegate)handler);
            }
            else
            {
                dictDelegates.Add(command, (MulticastDelegate)handler);
            }
        }

        public void RemoveHandler<TMessageContent>(EventHandler<MessageReceiveEventArgs<TMessageContent>> handler)
        {
            RemoveHandler<TMessageContent, object>(handler, null);
        }

        public void RemoveHandler<TMessageContent, TCommand>(EventHandler<MessageReceiveEventArgs<TMessageContent>> handler, TCommand command)
        {
            Type type = typeof(TMessageContent);
            Dictionary<object, MulticastDelegate> dictDelegates;
            if (!dictMessageHandlers.ContainsKey(type))
            {
                return;
            }
            else
            {
                dictDelegates = dictMessageHandlers[type];
            }
            if (dictDelegates.ContainsKey(command))
            {
                MulticastDelegate md = dictDelegates[type];
                dictDelegates[command] = (MulticastDelegate)MulticastDelegate.Remove(md, (MulticastDelegate)handler);
            }
            else
            {
                dictDelegates.Remove(command);
            }
            if (dictDelegates.Count == 0)
            {
                dictMessageHandlers.Remove(type);
            }
        }

        #endregion

        #region Message handling

        private void ReceiveLoop(IDuplexSessionChannel channel)
        {
            // Start listening for callbacks.
            IAsyncResult result = channel.BeginReceive(new AsyncCallback(OnReceiveComplete), channel);
            if (result.CompletedSynchronously)
            {
                CompleteReceive(result);
            }
        }

        private void OnMessageReceived(object msg)
        {
            string typeName = typeof(string).FullName;
            string asmName = typeof(string).AssemblyQualifiedName;
            int headerIndex = -1;
            object messageCommand = null;
            Message message = msg as Message;
            
            #region Extracting message type
            
            headerIndex = message.Headers.FindHeader("messageContentType", "forge.SBPweb.hu");
            if (headerIndex >= 0)
            {
                ExtractTypeFromHeader(message, headerIndex, out asmName, out typeName);
            }

            #endregion

            #region Extracting message command type

            string commandTypeName = typeof(string).FullName;
            string commandAsmName = typeof(string).AssemblyQualifiedName;
            headerIndex = message.Headers.FindHeader("messageCommandType", "forge.SBPweb.hu");
            if (headerIndex >= 0)
            {
                ExtractTypeFromHeader(message, headerIndex, out commandAsmName, out commandTypeName);
                Type commandType = Assembly.Load(commandAsmName).GetType(commandTypeName);

                #region Extracting message command

                headerIndex = message.Headers.FindHeader("messageCommand", "forge.SBPweb.hu");
                if (headerIndex >= 0)
                {
                    DataContractSerializer dS = new DataContractSerializer(commandType);
                    XmlDictionaryReader reader = message.Headers.GetReaderAtHeader(headerIndex);
                    messageCommand = dS.ReadObject(reader, false);
                }

                #endregion

            }

            #endregion

            Type type = Assembly.Load(asmName).GetType(typeName);

            DataContractSerializer serializer = new DataContractSerializer(type);
            object mb = message.GetBody<object>(serializer);
            Delegate del = GetDelegateFromCache(type, messageCommand);
            if (del == null)
            {
                return;
            }
            MessageReceiveEventArgs mrea = MessageReceiveEventArgs.CreateGenericInstance(type, mb);
            mrea.Command = messageCommand;
            del.DynamicInvoke(this, mrea);
        }

        private void OnOpenCompleteFactory(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
            {
                return;
            }
            else
            {
                CompleteOpenFactory(result);
            }
        }

        private void OnOpenCompleteChannel(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
            {
                return;
            }
            else
            {
                CompleteOpenChannel(result);
            }
        }

        private void OnCloseCompleteFactory(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
            {
                return;
            }
            else
            {
                CompleteCloseFactory(result);
            }
        }

        private void OnSend(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
            {
                return;
            }
            else
            {
                CompleteOnSend(result);
            }
        }

        private void OnReceiveComplete(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
            {
                return;
            }
            else
            {
                CompleteReceive(result);
            }
        }

        private void OnCloseChannel(IAsyncResult result)
        {
            if (result.CompletedSynchronously)
            {
                return;
            }
            else
            {
                CompleteCloseChannel(result);
            }
        }

        private void CompleteOpenFactory(IAsyncResult result)
        {
            IChannelFactory<IDuplexSessionChannel> factory = (IChannelFactory<IDuplexSessionChannel>)result.AsyncState;

            factory.EndOpen(result);

            // The factory is now open. Create and open a channel from the channel factory.
            IDuplexSessionChannel channel = factory.CreateChannel(new EndpointAddress(ServiceUrl));

            IAsyncResult channelOpenResult = channel.BeginOpen(new AsyncCallback(OnOpenCompleteChannel), channel);
            if (channelOpenResult.CompletedSynchronously)
            {
                CompleteOpenChannel(channelOpenResult);
            }
        }

        private void CompleteOpenChannel(IAsyncResult result)
        {
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;

            channel.EndOpen(result);

            string registerMessage = string.Format("SessionId={0};ActionData={1}", SessionId, ActionData);
            // Channel is now open. Send message
            Message message = Message.CreateMessage(channel.GetProperty<MessageVersion>(), Action, registerMessage);
            IAsyncResult resultChannel = channel.BeginSend(message, new AsyncCallback(OnSend), channel);
            if (resultChannel.CompletedSynchronously)
            {
                CompleteOnSend(resultChannel);
            }

            //Start listening for callbacks from the service
            ReceiveLoop(channel);
        }

        private void CompleteOnSend(IAsyncResult result)
        {
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
            channel.EndSend(result);
        }

        private void CompleteReceive(IAsyncResult result)
        {
            //A callback was received so process data
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;

            try
            {
                Message receivedMessage = channel.EndReceive(result);

                // Show the service response in the UI.
                if (receivedMessage != null)
                {
                    // string text = receivedMessage.GetBody<string>();
                    threadUI.Post(OnMessageReceived, receivedMessage);
                }

                ReceiveLoop(channel);
            }
            catch (CommunicationObjectFaultedException exp)
            {
                threadUI.Post(delegate(object msg)
                {
                    System.Windows.Browser.HtmlPage.Window.Alert(msg.ToString());
                }, exp.Message);
            }
        }

        private void CompleteCloseFactory(IAsyncResult result)
        {
            IChannelFactory<IDuplexSessionChannel> factory = (IChannelFactory<IDuplexSessionChannel>)result.AsyncState;
            factory.EndClose(result);
        }

        private void CompleteCloseChannel(IAsyncResult result)
        {
            IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState;
            channel.EndClose(result);
        }

        #endregion

        #region Private helpers

        internal static string GetServiceAbsolutePath(string relativeServiceUri)
        {
            string path = Application.Current.Host.Source.AbsoluteUri;
            return string.Format("{0}{1}", path.Substring(0, path.LastIndexOf("ClientBin") - 1), relativeServiceUri);
        }

        private void ExtractTypeFromHeader(Message message, int headerIndex, out string asmName, out string typeName)
        {
            XmlDictionaryReader reader = message.Headers.GetReaderAtHeader(headerIndex);
            reader.ReadStartElement();
            int depth = reader.Depth;
            typeName = string.Empty;
            asmName = string.Empty;
            for (int i = 0; i < depth; i++)
            {
                if (reader.Name == "Data")
                {
                    typeName = reader.ReadInnerXml();
                }
                if (reader.Name == "AssemblyName")
                {
                    asmName = reader.ReadInnerXml();
                }
                reader.Skip();
            }
        }

        private Delegate GetDelegateFromCache(Type type, object command)
        {
            Dictionary<object, MulticastDelegate> dict = dictMessageHandlers[type];
            if (command != null && dict.ContainsKey(command))
            {
                return dict[command];
            }
            else if(dict.ContainsKey(type))
            {
                return dict[type];
            }

            return null;
        }

        #endregion

        #endregion

    }

}
